package com.dmitriyp.movie.implementation;

import java.io.File;
import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Restrictions;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;
import org.hibernate.sql.JoinType;

import com.dmitriyp.movie.dao.IMovieManager;
import com.dmitriyp.movie.dao.Order;
import com.dmitriyp.movie.dao.Order.Type;
import com.dmitriyp.movie.mapping.Movie;

public class HibernateMovieManger implements IMovieManager {

    private static final String MOVIE_TITLE_FIELD = "title"; //$NON-NLS-1$
    private static final String MOVIE_STARS_FIELD = "stars"; //$NON-NLS-1$

    private final SessionFactory sessionFactory;

    public HibernateMovieManger(String filePath) {
        Configuration cfg = new Configuration().configure(filePath);
        ServiceRegistry serviceRegistry = new ServiceRegistryBuilder().applySettings(
                cfg.getProperties()).buildServiceRegistry();
        sessionFactory = cfg.buildSessionFactory(serviceRegistry);
    }

    public HibernateMovieManger(File file) {
        Configuration cfg = new Configuration().configure(file);
        ServiceRegistry serviceRegistry = new ServiceRegistryBuilder().applySettings(
                cfg.getProperties()).buildServiceRegistry();
        sessionFactory = cfg.buildSessionFactory(serviceRegistry);
    }

    public Movie get(final int movieId) {
        Movie movie = this.<Movie> doWork(new Operation<Movie>() {

            public Movie execute(Session session) {
                Movie movie = (Movie) session.get(Movie.class, movieId);
                Hibernate.initialize(movie.getStars());
                return (Movie) session.get(Movie.class, movieId);
            }

        });
        return movie;
    }

    public Movie add(final Movie movie) {
        this.<Integer> doWork(new Operation<Integer>() {

            public Integer execute(Session session) {
                Transaction tx = session.beginTransaction();
                if(movie.getReleaseYear() < 1) {
                	tx.rollback();
                	return -1;
                }
                int id = (Integer) session.save(movie);
                tx.commit();
                return id;
            }

        });
        return movie;
    }

    public boolean delete(final int movieId) {
        Boolean s = this.<Boolean> doWork(new Operation<Boolean>() {

            public Boolean execute(Session session) {
                Movie movie = get(movieId);
                if (movie == null)
                    return false;
                Transaction tx = session.beginTransaction();
                session.delete(movie);
                tx.commit();
                return true;
            }

        });
        return s;
    }

    public boolean delete(final Movie movie) {
        Boolean s = this.<Boolean> doWork(new Operation<Boolean>() {

            public Boolean execute(Session session) {
                Transaction tx = session.beginTransaction();
                session.delete(movie);
                tx.commit();
                return true;
            }

        });
        return s;
    }

    public List<Movie> listAll(final Order... orders) {
        List<Movie> movies = this.<List<Movie>> doWork(new Operation<List<Movie>>() {

            public List<Movie> execute(Session session) {
                Criteria criteria = session.createCriteria(Movie.class);
                for (Order order : orders) {
                    criteria = criteria.addOrder(convertToHibernateOrder(order));
                }
                @SuppressWarnings("unchecked")
                List<Movie> movies = (List<Movie>) criteria.list();
                return movies;
            }
        });
        return movies;
    }

    private org.hibernate.criterion.Order convertToHibernateOrder(Order order) {
        return order.getType() == Type.ASC ? org.hibernate.criterion.Order.asc(order
                .getPropertyName()) : org.hibernate.criterion.Order.desc(order.getPropertyName());
    }

    public List<Movie> filter(final String title) {
        List<Movie> result = this.<List<Movie>> doWork(new Operation<List<Movie>>() {

            @SuppressWarnings("unchecked")
            public List<Movie> execute(Session session) {
                Criteria criteria = session.createCriteria(Movie.class);
                criteria.add(Restrictions.like(MOVIE_TITLE_FIELD, title, MatchMode.ANYWHERE));
                return (List<Movie>) criteria.list();
            }

        });
        return result;
    }

    public List<Movie> filterByStar(final String star) {
        List<Movie> result = this.<List<Movie>> doWork(new Operation<List<Movie>>() {

            @SuppressWarnings("unchecked")
            public List<Movie> execute(Session session) {
                Criteria criteria = session.createCriteria(Movie.class)
                        .createAlias(MOVIE_STARS_FIELD, "strs", JoinType.LEFT_OUTER_JOIN)
                        .add(Restrictions.eq("strs.elements", star));
                return (List<Movie>) criteria.list();
            }

        });
        return result;
    }

    private <T> T doWork(Operation<T> op) {
        Session session = sessionFactory.openSession();
        T result = op.execute(session);
        session.close();
        return result;
    }

    /**
     * Interface that present abstraction of some job with {@link Session}.
     * 
     * @author dmitriy_p
     * 
     * @param <T>
     */
    private interface Operation<T> {

        /**
         * Caller have to open session before call and close it after.
         * 
         * @param session
         *            - Hibernate session
         * @return
         */
        T execute(Session session);

    }

}
